﻿// HSS.Interlink.UploadFileWriter.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.html)
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2010
// ----------------------------------------------------------------------------
// File:       UploadFileWriter.cs
// Author:     HSS\gbanta
// Created:    07/16/2011
// Modified:   07/22/2011
// ----------------------------------------------------------------------------
namespace HSS.Interlink
{
	#region Using Directives
	using System;
	using System.IO;
	using System.Net;
	using System.Security.Cryptography;
	using System.Windows;
	#endregion

	#region UploadFileWriter
	/// <summary>
	/// UploadFileWriter writes a FileStream to an *.ashx Handler on
	/// the web server.
	/// </summary>
	internal sealed class UploadFileWriter
	{
		#region Responses
		/// <summary>
		/// Responses from the Server.
		/// </summary>
		struct Responses
		{
			/// <summary>
			/// append-file-retry
			/// </summary>
			public const string AppendFileRetry = "append-file-retry";
			/// <summary>
			/// file-already-exists
			/// </summary>
			public const string FileAlreadyExists = "file-already-exists";
			/// <summary>
			/// file-does-not-exist
			/// </summary>
			public const string FileDoesNotExist = "file-does-not-exist";
		}
		#endregion

		#region Fields

		string handlerUri;
		string handlerKey;
		string metadata = string.Empty;
		string fileName;

		Guid jobId;
		UploadFileViewModel fileViewModel;
		Stopwatch watch;

		Action<string, Action<bool>> fileAlreadyExistsHandler;
		bool isCancelRequested;
		bool isComplete;
		bool isErrored;

		long fileLen;
		long totalBytesSent;
		long bytesSent;
		long chunkOffset = 0x1020;	// 4128 bytes
		long maxChunk = 0x3FEFE0;	// 4190176 = 4194304 bytes(4MB) - 4128 bytes (32 bytes for 256 BIT Hash  +  4KB spacer for buffer excess beyond chunkSize);
		long chunkSize = 1024;		// 1KB - Changed on the fly based on network performance
		long KB4 = 0x1000;			// 4096 bytes
		long KB1 = 0x400;			// 1024 bytes
		#endregion

		#region Constructor
		/// <summary>
		/// Constructor
		/// </summary>
		public UploadFileWriter()
		{
		}
		#endregion

		#region Methods
		/// <summary>
		/// Starts writing the file
		/// </summary>
		internal void BeginWrite(UploadFileViewModel viewModel, string metadata, Action<string, Action<bool>> handleFileAlreadyExists)
		{
			if (this.isComplete)
				return;

			this.jobId = Guid.NewGuid();
			this.fileAlreadyExistsHandler = handleFileAlreadyExists;
			this.totalBytesSent = 0;
			this.fileViewModel = viewModel;
			this.handlerKey = viewModel.HandlerKey;
			this.fileName = viewModel.FileName;
			this.metadata = metadata;
			this.fileLen = viewModel.FileLength;
			this.handlerUri = viewModel.UploadUri;
			this.watch = new Stopwatch();
			this.PrepareRequest();
		}
		/// <summary>
		/// Request to cancel the write operation
		/// </summary>
		internal void RequestCancel()
		{
			this.isCancelRequested = true;
		}

		#region Pipeline

		#region Prepare Request
		void PrepareRequest()
		{
			Uri uri = this.GetRequestUri(FileQueryCommand.Prepare);
			WebClient webClient = new WebClient();
			webClient.UploadStringCompleted += new UploadStringCompletedEventHandler(PrepareRequestCompleted);
			webClient.UploadStringAsync(uri, metadata);
		}
		void PrepareRequestCompleted(object sender, UploadStringCompletedEventArgs e)
		{
			if (!e.Cancelled)
			{
				if (null != e.Error)
				{
					this.SetFileError(e.Error);
					return;
				}

				string maxChunkResponse = e.Result;
				if (maxChunkResponse.Length > 0)
				{
					if (maxChunkResponse.StartsWith(Strings.Cookie_MaxChunkSize))
						this.SendFileCheck(maxChunkResponse);
					else
						this.SetFileError(new WebException(maxChunkResponse));
				}
				else
					this.SetFileError(new WebException("Missing response from Prepare Request."));
			}
			else
				this.SetFileError(new WebException(Strings.Ex_Canceled));
		}
		#endregion

		#region Check FileExist Request
		void SendFileCheck(string maxChunkResponse)
		{
			this.SetMaxChunkSize(maxChunkResponse);
			HttpWebRequest webRequest = this.CreateRequest(FileQueryCommand.CheckFileExists, Strings.HTTP_GET);
			webRequest.BeginGetResponse(new AsyncCallback(ReadFileCheckResponse), webRequest);
		}
		void ReadFileCheckResponse(IAsyncResult asynchronousResult)
		{
			//Check the response
			string response = string.Empty;
			try
			{
				HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState;
				HttpWebResponse webResponse = (HttpWebResponse)webRequest.EndGetResponse(asynchronousResult);
				using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
				{
					response = reader.ReadToEnd();
					reader.Close();
					webResponse.Close();
				}

				// Response should be one of the following...
				// file-already-exists || file-does-not-exist
				// If no response or not one of these two values than error.
				if (!string.IsNullOrEmpty(response))
				{
					switch (response)
					{
						case Responses.FileDoesNotExist: // OK, upload file unless canceled
							if (!this.isCancelRequested)
								this.SendNewFile();
							return;

						case Responses.FileAlreadyExists:
							if (null != this.fileAlreadyExistsHandler)
							{
								this.fileAlreadyExistsHandler(this.fileName, this.OverwriteFile);
								return;
							}
							this.SetFileError(new WebException(Strings.Ex_FileAlreadyExists));
							return;

						default: // an error message
							this.SetFileError(new WebException(response));
							return;
					}
				}
				else
				{
					// No response...
					this.SetFileError(new WebException(Strings.Ex_UnknownError));
					return;
				}
			}
			catch (Exception ex)
			{
				this.SetFileError(ex);
				return;
			}
		}
		#endregion

		#region NewFile
		void OverwriteFile(bool okToOverwrite)
		{
			if (this.isCancelRequested)
				return;
			if (okToOverwrite)
				this.SendNewFile();
			else
				this.SetFileError(new WebException(Strings.Ex_FileAlreadyExists));
		}
		void SendNewFile()
		{
			HttpWebRequest webRequest = this.CreateRequest(FileQueryCommand.NewFile, Strings.HTTP_GET);
			webRequest.BeginGetResponse(new AsyncCallback(ReadNewFileResponse), webRequest);
		}
		void ReadNewFileResponse(IAsyncResult asynchronousResult)
		{
			string response = string.Empty;
			try
			{
				HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState;
				HttpWebResponse webResponse = (HttpWebResponse)webRequest.EndGetResponse(asynchronousResult);

				using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
				{
					response = reader.ReadToEnd();
					reader.Close();
					webResponse.Close();
				}

				// If we have a response, then exception
				if (response.Length > 0)
					this.SetFileError(new WebException(response));
				else if (this.isCancelRequested)
					this.SendCancel();
				else
					this.AppendChunk();
			}
			catch (Exception ex)
			{
				this.SetFileError(ex);
				this.SendCancel();
			}
		}
		#endregion

		#region Append Chunk
		void AppendChunk()
		{
			#region Auto adjust chunking based on transfer rate and elapsed time
			if (bytesSent > 0)
			{
				double totalSeconds = this.watch.Elapsed.TotalSeconds;
				if (totalSeconds == 0)
					totalSeconds = .001;
				double transferRate = bytesSent / totalSeconds;
				this.SetChunkSize(transferRate);
			}
			#endregion

			HttpWebRequest webRequest = this.CreateRequest(FileQueryCommand.Append, Strings.HTTP_POST);
			webRequest.BeginGetRequestStream(new AsyncCallback(WriteChunk), webRequest);
		}
		void RetryLastChunk(long lastChunk)
		{
			this.totalBytesSent -= lastChunk;
			this.AppendChunk();
		}
		void WriteChunk(IAsyncResult asynchronousResult)
		{
			try
			{
				#region Write Chunk
				HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState;
				using (Stream requestStream = webRequest.EndGetRequestStream(asynchronousResult))
				{
					#region Set the start position and buffer size
					fileViewModel.FileStream.Position = this.totalBytesSent;
					byte[] buffer = new byte[KB4];
					long remainingBytes = (this.fileLen - totalBytesSent);
					if (buffer.Length > remainingBytes)
						buffer = new byte[remainingBytes];
					bytesSent = 0;
					#endregion

					#region Write the chunk
					SHA256Managed sha256 = new SHA256Managed();
					CryptoStream hashStm = new CryptoStream(Stream.Null, sha256, CryptoStreamMode.Write);
					int bytesRead = fileViewModel.FileStream.Read(buffer, 0, buffer.Length);
					while (bytesRead != 0)
					{
						#region Write the file bytes
						hashStm.Write(buffer, 0, bytesRead);
						requestStream.Write(buffer, 0, bytesRead);
						this.bytesSent += bytesRead;
						this.totalBytesSent += bytesRead;
						#endregion

						#region Should we continue

						if (fileViewModel.State == FileStates.Error)
							break; // Stop writing and get response

						if (this.isCancelRequested)
						{
							webRequest.Abort();
							this.SendCancel();
							return; // Stop writing and DO NOT get the response
						}

						if (bytesSent >= chunkSize)
						{
							// Stop writing and get response
							break;
						}

						#endregion

						// Next...
						bytesRead = fileViewModel.FileStream.Read(buffer, 0, buffer.Length);
					}

					#region Write the Hash

					// Close the hash stream
					hashStm.Close();
					hashStm = null;

					// Write the hash to the end of the chunk
					requestStream.Write(sha256.Hash, 0, 32);

					// Release Hash resources
					sha256.Clear();
					sha256 = null;

					#endregion

					// Must close the Request Stream
					requestStream.Close();

					#endregion
				}
				#endregion

				#region Send chunk
				this.watch.Reset();
				webRequest.BeginGetResponse(new AsyncCallback(ReadChunkResponse), webRequest);
				this.watch.Start();
				#endregion
			}
			catch (Exception ex)
			{
				this.SetFileError(ex);
				this.SendCancel();
			}
		}
		void ReadChunkResponse(IAsyncResult asynchronousResult)
		{
			string response = string.Empty;

			try
			{
				#region Read Response
				HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState;
				HttpWebResponse webResponse = (HttpWebResponse)webRequest.EndGetResponse(asynchronousResult);
				this.watch.Stop();
				using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
				{
					response = reader.ReadToEnd();
					reader.Close();
					webResponse.Close();
					webRequest = null;
				}
				#endregion

				#region Debug Output
				if (fileViewModel.IsDebugWriting)
				{
					double totalSeconds = this.watch.Elapsed.TotalSeconds;
					long transferRate = Convert.ToInt64(bytesSent / totalSeconds);

					WriteDebugLine("**********************************************");
					WriteDebugLine(fileViewModel.FileName + " Activity");
					WriteDebugLine("----------------------------------------------");
					WriteDebugLine("Elapsed Time:	" + totalSeconds.ToString());
					WriteDebugLine("Transfer Rate:	" + transferRate.ToString() + "	(" + (transferRate / 1024) + " KB/s)");
					WriteDebugLine("Bytes Sent:	" + bytesSent.ToString() + "	(" + (bytesSent / 1024) + " KB)");
				}
				#endregion

				#region Check response
				if (response.Length > 0)
				{
					switch (response)
					{
						case Responses.AppendFileRetry: // Chunk failed, and server requesting retry...
							if (!this.isCancelRequested)
								this.RetryLastChunk(bytesSent);
							else
								this.SendCancel();
							return;

						default: // assume it's an error message
							this.SetFileError(new WebException(response));
							this.SendCancel();
							return;
					}
				}
				#endregion
			}
			catch (Exception ex)
			{
				this.SetFileError(ex);
				this.SendCancel();
				return;
			}
			finally
			{
				System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
					{
						this.fileViewModel.UploadProgress(this.totalBytesSent, this.bytesSent);
					});
			}

			// SendCancel if a Cancel has been requested
			if (this.isCancelRequested)
			{
				this.SendCancel();
				return;
			}

			if (totalBytesSent < fileLen)
				// If more data, send next chunk
				this.AppendChunk();
			else
				// Send complete request
				this.SendComplete();

		}
		#endregion

		#region Send Cancel
		void SendCancel()
		{
			this.OnCanceling();
			HttpWebRequest webRequest = this.CreateRequest(FileQueryCommand.CancelUpload, Strings.HTTP_GET);
			webRequest.BeginGetResponse(new AsyncCallback(ReadCancelResponse), webRequest);
		}
		void ReadCancelResponse(IAsyncResult asynchronousResult)
		{
			try
			{
				HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState;
				HttpWebResponse webResponse = (HttpWebResponse)webRequest.EndGetResponse(asynchronousResult);
				using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
				{
					string response = reader.ReadToEnd();
					reader.Close();
					webResponse.Close();
				}
			}
			catch (Exception ex)
			{
				WriteDebugLine(ex.ToString());
			}
			this.SetFileCanceled();
		}
		#endregion

		#region Send Complete
		void SendComplete()
		{
			HttpWebRequest webRequest = this.CreateRequest(FileQueryCommand.UploadComplete, Strings.HTTP_GET);
			webRequest.BeginGetResponse(new AsyncCallback(ReadCompleteResponse), webRequest);
		}
		void ReadCompleteResponse(IAsyncResult asynchronousResult)
		{
			string response = string.Empty;
			try
			{
				HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState;
				HttpWebResponse webResponse = (HttpWebResponse)webRequest.EndGetResponse(asynchronousResult);
				using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
				{
					response = reader.ReadToEnd();
					reader.Close();
					webResponse.Close();
				}
			}
			catch (Exception ex)
			{
				WriteDebugLine(ex.ToString());
			}

			// And we're done!
			this.SetFileFinished(response);
		}
		#endregion

		#endregion

		#region Helpers
		private void SetMaxChunkSize(string maxChunkResponse)
		{
			if (!string.IsNullOrEmpty(maxChunkResponse))
			{
				maxChunkResponse = maxChunkResponse.Replace(Strings.Cookie_MaxChunkSize, string.Empty);
				long newMax = long.Parse(maxChunkResponse);
				if (newMax > 0)
				{
					newMax = newMax * KB1; // Convert to bytes
					maxChunk = newMax - chunkOffset; // - chunkOffset = (4128 bytes = 32 bytes for 256 BIT Hash  +  4KB spacer for buffer potential excess beyond chunkSize);
				}
			}
		}
		private void SetChunkSize(double transferRate)
		{
			chunkSize = Convert.ToInt64(transferRate);

			if (!fileViewModel.IsDebugWriting)
			{
				// Limit to maxChunk
				if (chunkSize > maxChunk)
					chunkSize = maxChunk;
			}
			else
			{
				// Limit to maxChunk
				if (chunkSize > maxChunk)
				{
					WriteDebugLine("Next Chunk:	" + chunkSize + "	(" + (chunkSize / 1024) + " KB) Too Big");
					chunkSize = maxChunk;
					WriteDebugLine("Next Chunk:	" + chunkSize + "	(" + (chunkSize / 1024) + " KB) Adjusted");
				}
				else
					WriteDebugLine("Next Chunk:	" + chunkSize + "	(" + (chunkSize / 1024) + " KB)");
				WriteDebugLine("");
			}
		}
		private void SetFileError(Exception ex)
		{
			this.isErrored = true;
			this.isComplete = true;
			this.OnError(ex);
			this.OnCompleted();
		}
		private void SetFileCanceled()
		{
			bool completed = this.isComplete;
			if (!completed)
				this.isComplete = true;
			if (!this.isErrored)
				this.OnCanceled();
			if (!completed)
				this.OnCompleted();
			this.isCancelRequested = false;
		}
		private void SetFileFinished(string result)
		{
			this.isComplete = true;
			this.OnFinished();
			this.OnCompleted(result);
		}
		private void WriteDebugLine(string message)
		{
			if (fileViewModel.IsDebugWriting)
				fileViewModel.DebugString.AppendLine(message);
		}
		private void OnError(Exception ex)
		{
			this.fileViewModel.UploadError(ex.Message);
		}
		private void OnCanceling()
		{
			if (!this.isErrored)
				this.fileViewModel.UploadCanceling();
		}
		private void OnCanceled()
		{
			this.fileViewModel.UploadCanceled();
		}
		private void OnFinished()
		{
			this.fileViewModel.UploadFinished();
		}
		private void OnCompleted(string result = "")
		{
			this.fileViewModel.UploadCompleted(this.totalBytesSent, result);
		}
		private HttpWebRequest CreateRequest(FileQueryCommand command, string method)
		{
			Uri uri = this.GetRequestUri(command);
			HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri);
			webRequest.Method = method;
			return webRequest;
		}
		private Uri GetRequestUri(FileQueryCommand command)
		{
			return new Uri(this.handlerUri + this.GetQuery(command));
		}
		private string GetQuery(FileQueryCommand command)
		{
			return FileQuery.CreateUploadFileQuery(this.handlerKey, this.jobId, this.fileName, command);
		}
		#endregion

		#endregion
	}
	#endregion
}